/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.andtinder.view;

import com.andtinder.model.LinearRegression;
import ohos.agp.animation.Animator;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.agp.utils.Point;
import ohos.multimodalinput.event.MmiPoint;
import ohos.multimodalinput.event.TouchEvent;

import java.security.SecureRandom;

public class SwipeCardListener implements Component.TouchEventListener {
    private static final int INVALID_POINTER_ID = -1;

    private final float mObjectX;
    private final float mObjectY;
    private final int mObjectH;
    private final int mObjectW;
    private final int mParentWidth;
    private final FlingListener mFlingListener;
    private final Object mDataObject;
    private final float mHalfWidth;
    private float baseRotationDegrees;

    private double mPosX;
    private double mPosY;
    private float mDownTouchX;
    private float mDownTouchY;
    private SecureRandom random = new SecureRandom();
    private boolean isNeedRotation = true;

    // The active pointer is the one currently moving our object.
    private int mActivePointerId = INVALID_POINTER_ID;
    private Component mFrame = null;
    private float maxCos = (float) Math.cos(Math.toRadians(45));

    /**
     * constructor
     *
     * @param parent
     * @param frame
     * @param itemAtPosition
     * @param flingListener
     */
    public SwipeCardListener(
            ComponentContainer parent, Component frame, Object itemAtPosition, FlingListener flingListener) {
        this(parent, frame, itemAtPosition, CardContainer.rotationDegrees, flingListener);
    }

    /**
     * constructor
     *
     * @param parent
     * @param frame
     * @param itemAtPosition
     * @param rotationDegrees
     * @param flingListener
     */
    public SwipeCardListener(
            ComponentContainer parent,
            Component frame,
            Object itemAtPosition,
            float rotationDegrees,
            FlingListener flingListener) {
        super();
        this.mFrame = frame;
        this.mObjectX = frame.getContentPositionX();
        this.mObjectY = frame.getContentPositionY();
        this.mObjectH = frame.getHeight();
        this.mObjectW = frame.getWidth();
        this.mHalfWidth = mObjectW / 2f;
        this.mDataObject = itemAtPosition;
        this.mParentWidth = parent.getWidth();
        this.baseRotationDegrees = rotationDegrees;
        this.mFlingListener = flingListener;
    }

    @Override
    public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
        MmiPoint point = touchEvent.getPointerScreenPosition(0);
        switch (touchEvent.getAction()) {
            case TouchEvent.PRIMARY_POINT_DOWN:
                float downTouchX = point.getX();
                float downTouchY = point.getY();
                mDownTouchX = downTouchX;
                mDownTouchY = downTouchY;

                if (mPosX == 0) {
                    mPosX = mFrame.getContentPositionX();
                }
                if (mPosY == 0) {
                    mPosY = mFrame.getContentPositionY();
                }
                break;
            case TouchEvent.PRIMARY_POINT_UP:
                mActivePointerId = INVALID_POINTER_ID;
                resetCardViewOnStack();
                break;
            case TouchEvent.OTHER_POINT_DOWN:
                break;
            case TouchEvent.OTHER_POINT_UP:
                break;
            case TouchEvent.POINT_MOVE:
                if (mDownTouchX == 0) {
                    return false;
                }
                final float moveTouchX = point.getX();
                final float moveTouchY = point.getY();
                final double dx = (double)moveTouchX - (double)mDownTouchX;
                final double dy = (double)moveTouchY - (double)mDownTouchY;
                mPosX += dx;
                mPosY += dy;
                // calculate the rotation degrees
                double distobjectX = mPosX - mObjectX;
                double rotation = (double)baseRotationDegrees * (double)2.f * (double)distobjectX / (double)mParentWidth;
                mFrame.setTranslationX((float) ((double)mFrame.getContentPositionX() + dx));
                mFrame.setTranslationY((float) ((double)mFrame.getContentPositionY() + dy));
                mFrame.setRotation((float)rotation);

                // save the touch point position
                mDownTouchX = moveTouchX;
                mDownTouchY = moveTouchY;
                break;
            case TouchEvent.CANCEL:
                mActivePointerId = INVALID_POINTER_ID;
                break;
            default:
                break;
        }
        return true;
    }

    private boolean resetCardViewOnStack() {
        if (movedBeyondLeftBorder()) {
            // Left Swipe
            onSelected(true, getExitPoint(-mObjectW), 100);
        } else if (movedBeyondRightBorder()) {
            // Right Swipe
            onSelected(false, getExitPoint(mParentWidth), 100);
        } else {
            double abslMoveDistance = Math.abs(mPosX - (double) mObjectX);
            if (abslMoveDistance < 4.0) {
                mFlingListener.onClick(mDataObject);
                if (!isNeedRotation) {
                    isNeedRotation = true;
                    return false;
                }
            }
            mFrame.createAnimatorProperty()
                    .moveFromX((float) mPosX)
                    .moveFromY((float)mPosY)
                    .setDuration(300)
                    .setCurveType(Animator.CurveType.LINEAR)
                    .moveToX(mObjectX)
                    .moveToY(mObjectY)
                    .rotate((float) Math.toDegrees(random.nextGaussian() * BaseCardContainer.DISORDERED_MAX_ROTATION_RADIANS))
                    .start();
            mPosX = 0;
            mPosY = 0;
            mDownTouchX = 0;
            mDownTouchY = 0;
        }
        return false;
    }

    private boolean movedBeyondLeftBorder() {
        return Math.abs(mPosX) > (mObjectW / 10f) && mPosX < 0;
    }

    private boolean movedBeyondRightBorder() {
        return Math.abs(mPosX) > (mObjectW / 10f) && mPosX > 0;
    }

    /**
     * calculate left border
     *
     * @return left border value
     */
    public float leftBorder() {
        return mParentWidth / 2.f;
    }

    /**
     * calculate right border
     *
     * @return right border value
     */
    public float rightBorder() {
        return 3 * mParentWidth / 7.f;
    }

    /**
     * the card on selected
     *
     * @param isLeft   isLeft
     * @param exitY    exitY
     * @param duration duration
     */
    public void onSelected(final boolean isLeft, float exitY, long duration) {
        double exitX;
        if (isLeft) {
            exitX = -mObjectW - (double)getRotationWidthOffset();
        } else {
            exitX = mParentWidth + (double)getRotationWidthOffset();
        }

        this.mFrame.createAnimatorProperty()
                .setDuration(300)
                .alpha(0)
                .setCurveType(Animator.CurveType.ACCELERATE)
                .moveByX((float) exitX)
                .moveByY(0)
                .setStateChangedListener(new Animator.StateChangedListener() {
                    @Override
                    public void onStart(Animator animator) {
                    }

                    @Override
                    public void onStop(Animator animator) {
                    }

                    @Override
                    public void onCancel(Animator animator) {
                    }

                    @Override
                    public void onEnd(Animator animator) {
                        if (isLeft) {
                            mFlingListener.onCardExited();
                            mFlingListener.leftExit(mDataObject);
                        } else {
                            mFlingListener.onCardExited();
                            mFlingListener.rightExit(mDataObject);
                        }
                    }

                    @Override
                    public void onPause(Animator animator) {
                    }

                    @Override
                    public void onResume(Animator animator) {
                    }
                })
                .rotate(0)
                .start();
    }

    private float getExitPoint(int exitxpoint) {
        float[] x = new float[2];
        x[0] = mObjectX;
        x[1] = (float) mPosX;

        float[] y = new float[2];
        y[0] = mObjectY;
        y[1] = (float) mPosY;

        LinearRegression regression = new LinearRegression(x, y);

        // Your typical y = ax+b linear regression
        return (float)((double) regression.slope() * (double) exitxpoint + (double) regression.intercept());
    }

    /**
     * When the object rotates it's width becomes bigger.
     * The maximum width is at 45 degrees.
     * <p/>
     * The below method calculates the width offset of the rotation.
     *
     * @return rotation width offset
     */
    private float getRotationWidthOffset() {
        return (float) ((double) mObjectW /(float) maxCos - (double)mObjectW);
    }

    public void setRotationDegrees(float degrees) {
        this.baseRotationDegrees = degrees;
    }

    public boolean isTouching() {
        return this.mActivePointerId != INVALID_POINTER_ID;
    }

    public Point getLastPoint() {
        return new Point((float)this.mPosX, (float)this.mPosY);
    }

    public boolean isNeedRotation() {
        return isNeedRotation;
    }

    public void setNeedRotation(boolean needRotation) {
        isNeedRotation = needRotation;
    }

    protected interface FlingListener {
        /**
         * on card exit
         */
        void onCardExited();

        /**
         * card onclick
         *
         * @param dataObject
         */
        void onClick(Object dataObject);

        /**
         * left exit
         *
         * @param dataObject
         */
        void leftExit(Object dataObject);

        /**
         * right exit
         *
         * @param dataObject
         */
        void rightExit(Object dataObject);
    }
}
